S01-20 JavaSE-项目:坦克大战
[TOC]
坦克大战游戏演示
游戏演示
(文档中提及游戏演示,无具体截图,保留原说明)
为什么写这个项目
- 涵盖 Java 多方面技术:面向对象、多线程、文件 I/O、数据库
- 巩固旧知识,学习新知识
- 趣味性强
写项目前的提醒
- 需具备一定 Java 基础,核心部分将逐步讲解
- 编程高手秘诀:思考 → 编程 → 思考 → 编程
授课原则
- 通俗易懂
- 不遗漏细节
- 版本迭代式开发(从 1.0 到最终版)
Java 绘图坐标体系
坐标体系-介绍
- 坐标原点位于左上角,以像素为单位
- 第一个参数为
x坐标(水平方向,距离原点像素数) - 第二个参数为
y坐标(垂直方向,距离原点像素数)
坐标体系-像素
- 像素是屏幕显示的基本单位
- 分辨率示例:800x600 表示屏幕每行 800 个像素,共 600 行(总计 480,000 个像素)
- 像素是密度单位,厘米是长度单位,不可直接比较
绘图快速入门(DrawCircle.java)
java
package com.hspedu.draw;
import javax.swing.*;
import java.awt.*;
/**
* 演示如何在面板上画出圆形
* @author 韩顺平
* @version 1.0
*/
@SuppressWarnings({"all"})
public class DrawCircle extends JFrame {
// 定义面板
private MyPanel mp = null;
public static void main(String[] args) {
new DrawCircle();
System.out.println("退出程序~");
}
public DrawCircle() { // 构造器
// 初始化面板
mp = new MyPanel();
// 把面板放入窗口
this.add(mp);
// 设置窗口大小
this.setSize(400, 300);
// 点击窗口关闭按钮,程序完全退出
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 显示窗口
this.setVisible(true);
}
// 自定义面板类,继承 JPanel,用于绘图
class MyPanel extends JPanel {
/**
* 绘图方法
* @param g 画笔对象
*/
@Override
public void paint(Graphics g) {
super.paint(g); // 调用父类方法完成初始化
System.out.println("paint 方法被调用了~");
// 1. 画直线 drawLine(int x1, int y1, int x2, int y2)
// g.drawLine(10, 10, 100, 100);
// 2. 画矩形边框 drawRect(int x, int y, int width, int height)
// g.drawRect(10, 10, 100, 100);
// 3. 填充矩形 fillRect(int x, int y, int width, int height)
g.setColor(Color.blue);
// g.fillRect(10, 10, 100, 100);
// 4. 画椭圆边框 drawOval(int x, int y, int width, int height)
// g.drawOval(10, 10, 100, 100);
// 5. 填充椭圆 fillOval(int x, int y, int width, int height)
g.setColor(Color.red);
// g.fillOval(10, 10, 100, 100);
// 6. 画图片 drawImage(Image img, int x, int y, ...)
// Image image = Toolkit.getDefaultToolkit().getImage(Panel.class.getResource("/bg.png"));
// g.drawImage(image, 10, 10, 175, 221, this);
// 7. 画字符串 drawString(String str, int x, int y)
g.setColor(Color.red);
g.setFont(new Font("隶书", Font.BOLD, 50));
g.drawString("北京你好", 100, 100);
}
}
}绘图原理
Component类提供核心绘图方法:paint(Graphics g): 绘制组件外观repaint(): 刷新组件外观
paint()自动调用场景:- 组件第一次显示时
- 窗口最小化后最大化
- 窗口大小改变时
- 调用
repaint()方法时
Graphics 类(画笔)
核心方法列表:
| 方法 | 功能描述 |
|---|---|
drawLine(int x1, int y1, int x2, int y2) | 画直线 |
drawRect(int x, int y, int width, int height) | 画矩形边框 |
drawOval(int x, int y, int width, int height) | 画椭圆边框 |
fillRect(int x, int y, int width, int height) | 填充矩形 |
fillOval(int x, int y, int width, int height) | 填充椭圆 |
drawImage(Image img, int x, int y, ...) | 画图片 |
drawString(String str, int x, int y) | 画字符串 |
setFont(Font font) | 设置字体 |
setColor(Color c) | 设置颜色 |
绘出坦克(坦克大战 1.0 版)
核心代码(HspTankGame01.java)
java
package com.hspedu.tankgame;
import javax.swing.*;
import java.awt.*;
/**
* 坦克大战游戏主窗口
* @author 韩顺平
* @version 1.0
*/
public class HspTankGame01 extends JFrame {
// 定义绘图面板
MyPanel mp = null;
public static void main(String[] args) {
new HspTankGame01();
}
public HspTankGame01() {
mp = new MyPanel();
this.add(mp); // 加入面板
this.setSize(1000, 750); // 窗口大小
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // 关闭窗口退出程序
this.setVisible(true); // 显示窗口
}
}
/**
* 坦克大战绘图区域
* @author 韩顺平
* @version 1.0
*/
class MyPanel extends JPanel {
// 定义我方坦克
Hero hero = null;
public MyPanel() {
hero = new Hero(100, 100); // 初始化坦克位置
}
@Override
public void paint(Graphics g) {
super.paint(g);
g.fillRect(0, 0, 1000, 750); // 填充背景(黑色)
// 绘制坦克
drawTank(hero.getX(), hero.getY(), g, 0, 0);
}
/**
* 绘制坦克方法
* @param x 坦克左上角 x 坐标
* @param y 坦克左上角 y 坐标
* @param g 画笔
* @param direct 坦克方向(0-上,1-右,2-下,3-左)
* @param type 坦克类型(0-我方,1-敌人)
*/
public void drawTank(int x, int y, Graphics g, int direct, int type) {
// 设置坦克颜色
switch (type) {
case 0: // 我方坦克(青色)
g.setColor(Color.cyan);
break;
case 1: // 敌人坦克(黄色)
g.setColor(Color.yellow);
break;
}
// 根据方向绘制坦克
switch (direct) {
case 0: // 向上
g.fill3DRect(x, y, 10, 60, false); // 左轮子
g.fill3DRect(x + 30, y, 10, 60, false); // 右轮子
g.fill3DRect(x + 10, y + 10, 20, 40, false); // 坦克身体
g.fillOval(x + 10, y + 20, 20, 20); // 坦克盖子
g.drawLine(x + 20, y + 30, x + 20, y); // 炮筒
break;
// 其他方向(右、下、左)省略,完整代码见后续版本
default:
System.out.println("暂时没有处理");
}
}
}
/**
* 坦克父类
* @author 韩顺平
* @version 1.0
*/
class Tank {
private int x; // 横坐标
private int y; // 纵坐标
public Tank(int x, int y) {
this.x = x;
this.y = y;
}
// getter 和 setter 方法
public int getX() { return x; }
public void setX(int x) { this.x = x; }
public int getY() { return y; }
public void setY(int y) { this.y = y; }
}
/**
* 我方坦克类
* @author 韩顺平
* @version 1.0
*/
class Hero extends Tank {
public Hero(int x, int y) {
super(x, y);
}
}绘图练习
运用 Java 绘图技术绘制以下图形:
- 蛤蟆
- 王八
- 小老鼠
Java 事件处理机制
案例:键盘控制小球移动(BallMove.java)
java
package com.hspedu.event_;
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
/**
* 演示键盘控制小球移动
* @author 韩顺平
* @version 1.0
*/
public class BallMove extends JFrame {
MyPanel mp = null;
public static void main(String[] args) {
new BallMove();
}
public BallMove() {
mp = new MyPanel();
this.add(mp);
this.setSize(400, 300);
// 窗口监听键盘事件
this.addKeyListener(mp);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setVisible(true);
}
// 面板类,实现 KeyListener 接口
class MyPanel extends JPanel implements KeyListener {
int x = 10; // 小球左上角 x 坐标
int y = 10; // 小球左上角 y 坐标
@Override
public void paint(Graphics g) {
super.paint(g);
g.fillOval(x, y, 20, 20); // 绘制小球
}
// 有字符输入时触发(未使用)
@Override
public void keyTyped(KeyEvent e) {}
// 按键按下时触发
@Override
public void keyPressed(KeyEvent e) {
// 根据按键控制小球移动
if (e.getKeyCode() == KeyEvent.VK_DOWN) { // 下箭头
y++;
} else if (e.getKeyCode() == KeyEvent.VK_UP) { // 上箭头
y--;
} else if (e.getKeyCode() == KeyEvent.VK_LEFT) { // 左箭头
x--;
} else if (e.getKeyCode() == KeyEvent.VK_RIGHT) { // 右箭头
x++;
}
this.repaint(); // 重绘面板
}
// 按键松开时触发(未使用)
@Override
public void keyReleased(KeyEvent e) {}
}
}基本说明
- Java 事件处理采用委派事件模型
- 事件发生时,事件源将事件信息传递给事件监听者处理
- 事件对象保存事件相关信息(如
KeyEvent包含按键编码)
事件处理机制示意图
用户操作 → 事件源 → 事件对象 → 事件监听者 → 事件处理方法核心概念
- 事件源:产生事件的对象(如按钮、窗口、键盘)
- 事件:承载事件源状态改变的对象,常见类型如下:
| 事件类型 | 说明 |
|---|---|
ActionEvent | 按下按钮、双击列表项、选中菜单时发生 |
AdjustmentEvent | 操作滚动条时发生 |
ComponentEvent | 组件隐藏、移动、改变大小时发生 |
ContainerEvent | 组件加入/删除容器时发生 |
FocusEvent | 组件获得/失去焦点时发生 |
ItemEvent | 复选框、列表项被选中时发生 |
KeyEvent | 键盘按键按下/松开时发生 |
MouseEvent | 鼠标拖动、移动、点击时发生 |
TextEvent | 文本区/文本框内容改变时发生 |
WindowEvent | 窗口激活、关闭、最小化时发生 |
- 事件监听器:实现事件监听器接口的类,用于处理事件
- 一个类可实现多个监听器接口
- 接口位于
java.awt.event和javax.swing.event包
坦克大战游戏(2.0 版)
功能:按键控制坦克移动(W/D/S/A)
核心代码(HspTankGame02.java)
java
package com.hspedu.tankgame2;
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Vector;
/**
* 坦克大战 2.0 版(支持坦克移动和敌人坦克绘制)
* @author 韩顺平
* @version 1.0
*/
public class HspTankGame02 extends JFrame {
MyPanel mp = null;
public static void main(String[] args) {
new HspTankGame02();
}
public HspTankGame02() {
mp = new MyPanel();
this.add(mp);
this.setSize(1000, 750);
this.addKeyListener(mp); // 监听键盘事件
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setVisible(true);
}
}
/**
* 敌人坦克类
* @author 韩顺平
* @version 1.0
*/
class EnemyTank extends Tank {
public EnemyTank(int x, int y) {
super(x, y);
}
}
/**
* 我方坦克类
* @author 韩顺平
* @version 1.0
*/
class Hero extends Tank {
public Hero(int x, int y) {
super(x, y);
}
}
/**
* 绘图面板(实现键盘监听)
* @author 韩顺平
* @version 1.0
*/
class MyPanel extends JPanel implements KeyListener {
Hero hero = null;
// 敌人坦克集合(Vector 线程安全)
Vector<EnemyTank> enemyTanks = new Vector<>();
int enemyTankSize = 3; // 敌人坦克数量
public MyPanel() {
hero = new Hero(100, 100); // 初始化我方坦克
// 初始化敌人坦克
for (int i = 0; i < enemyTankSize; i++) {
EnemyTank enemyTank = new EnemyTank(100 * (i + 1), 0);
enemyTank.setDirect(2); // 设置方向向下
enemyTanks.add(enemyTank);
}
}
@Override
public void paint(Graphics g) {
super.paint(g);
g.fillRect(0, 0, 1000, 750); // 背景
// 绘制我方坦克(type=1)
drawTank(hero.getX(), hero.getY(), g, hero.getDirect(), 1);
// 绘制敌人坦克(type=0)
for (int i = 0; i < enemyTanks.size(); i++) {
EnemyTank enemyTank = enemyTanks.get(i);
drawTank(enemyTank.getX(), enemyTank.getY(), g, enemyTank.getDirect(), 0);
}
}
/**
* 绘制坦克(支持多方向)
*/
public void drawTank(int x, int y, Graphics g, int direct, int type) {
// 设置颜色
switch (type) {
case 0: // 敌人坦克(青色)
g.setColor(Color.cyan);
break;
case 1: // 我方坦克(黄色)
g.setColor(Color.yellow);
break;
}
// 根据方向绘制
switch (direct) {
case 0: // 向上
g.fill3DRect(x, y, 10, 60, false); // 左轮
g.fill3DRect(x + 30, y, 10, 60, false); // 右轮
g.fill3DRect(x + 10, y + 10, 20, 40, false); // 车身
g.fillOval(x + 10, y + 20, 20, 20); // 盖子
g.drawLine(x + 20, y + 30, x + 20, y); // 炮筒
break;
case 1: // 向右
g.fill3DRect(x, y, 60, 10, false); // 上轮
g.fill3DRect(x, y + 30, 60, 10, false); // 下轮
g.fill3DRect(x + 10, y + 10, 40, 20, false); // 车身
g.fillOval(x + 20, y + 10, 20, 20); // 盖子
g.drawLine(x + 30, y + 20, x + 60, y + 20); // 炮筒
break;
case 2: // 向下
g.fill3DRect(x, y, 10, 60, false); // 左轮
g.fill3DRect(x + 30, y, 10, 60, false); // 右轮
g.fill3DRect(x + 10, y + 10, 20, 40, false); // 车身
g.fillOval(x + 10, y + 20, 20, 20); // 盖子
g.drawLine(x + 20, y + 30, x + 20, y + 60); // 炮筒
break;
case 3: // 向左
g.fill3DRect(x, y, 60, 10, false); // 上轮
g.fill3DRect(x, y + 30, 60, 10, false); // 下轮
g.fill3DRect(x + 10, y + 10, 40, 20, false); // 车身
g.fillOval(x + 20, y + 10, 20, 20); // 盖子
g.drawLine(x + 30, y + 20, x, y + 20); // 炮筒
break;
default:
System.out.println("暂时没有处理");
}
}
// 键盘监听方法
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_W) { // 向上
hero.setDirect(0);
hero.moveUp();
} else if (e.getKeyCode() == KeyEvent.VK_D) { // 向右
hero.setDirect(1);
hero.moveRight();
} else if (e.getKeyCode() == KeyEvent.VK_S) { // 向下
hero.setDirect(2);
hero.moveDown();
} else if (e.getKeyCode() == KeyEvent.VK_A) { // 向左
hero.setDirect(3);
hero.moveLeft();
}
this.repaint(); // 重绘
}
@Override
public void keyTyped(KeyEvent e) {}
@Override
public void keyReleased(KeyEvent e) {}
}
/**
* 坦克父类(增强移动方法)
* @author 韩顺平
* @version 1.0
*/
class Tank {
private int x;
private int y;
private int direct = 0; // 方向:0-上,1-右,2-下,3-左
private int speed = 1; // 移动速度
public Tank(int x, int y) {
this.x = x;
this.y = y;
}
// 移动方法
public void moveUp() { y -= speed; }
public void moveRight() { x += speed; }
public void moveDown() { y += speed; }
public void moveLeft() { x -= speed; }
// getter 和 setter
public int getX() { return x; }
public void setX(int x) { this.x = x; }
public int getY() { return y; }
public void setY(int y) { this.y = y; }
public int getDirect() { return direct; }
public void setDirect(int direct) { this.direct = direct; }
public int getSpeed() { return speed; }
public void setSpeed(int speed) { this.speed = speed; }
}本章作业
编程题(基于 HspTankGame02.java)
- 绘制三辆敌人坦克(颜色区分)
- 单独创建
EnemyTank类(后续扩展特殊属性和方法) - 使用
Vector存储敌人坦克(考虑多线程安全)
本章内容小结
| 版本 | 核心功能 |
|---|---|
| HspTankGame01.java | 绘制我方坦克 |
| HspTankGame02.java | 1. 绘制我方和敌人坦克 2. 按键(W/D/S/A)控制我方坦克移动 |
线程-应用到坦克大战
坦克大战 0.3 版(发射子弹)
核心思路
- 按下
J键发射子弹,子弹为独立线程 - 子弹移动到面板边界时销毁
- 面板不停重绘子弹,实现射击效果
坦克大战 0.4 版(增强功能)
- 敌人坦克发射子弹(多颗子弹,用
Vector存储) - 我方坦克击中敌人坦克后,敌人坦克消失(可选爆炸效果)
- 敌人坦克随机移动(实现
Runnable接口) - 限制坦克移动范围
- 扩展:我方坦克最多发射5颗子弹,敌人坦克最多发射3颗子弹
敌人坦克发射子弹思路
EnemyTank类中用Vector存储Shot对象- 创建
EnemyTank时初始化Shot并启动线程 - 面板绘制时遍历
Vector,绘制所有子弹 - 子弹消亡(
isLive = false)时从Vector移除
我方坦克发射限制思路
- 用
Vector存储我方子弹 - 按下
J键时判断子弹数量是否超过上限 - 子弹消亡后从
Vector移除
IO流-应用到坦克大战
坦克大战0.5版 新增功能
- 防止敌人坦克重叠运动
- 记录玩家击毁敌方坦克数,存盘退出
- 记录敌人坦克坐标/方向,存盘退出
- 支持选择开新游戏或继续上局游戏
核心思路
- 数据持久化:使用IO流将击毁数、敌人坦克信息写入文件(myRecord.txt)
- 数据恢复:游戏启动时读取文件,恢复上局进度
- 防止重叠:通过坐标判断敌人坦克位置,避免重叠
坦克大战0.6版 新增功能
- 游戏开始时播放音乐
- 修正文件存储位置
- 处理文件相关异常,提高代码健壮性